/*jslint nomen: true, plusplus: true, regexp: true, todo: true, browser: true, devel: true, indent: 2 */

/**

An EcmaScript string template handler for the proposed E4H syntax extension
that shows that it can be implemented via string templates.

A string template aware EcmaScript interpreter could have this loaded to allow

   e4h`<>Foo...</>`

to be used in an expression context to produce a DOM node.

The author does not endorse the E4H standard, but merely hopes to demonstrate
that it is compatible with string templates.

The grammar below is derived from http://www.hixie.ch/specs/e4h/strawman but
without the vestigial "Literal" production, and where the "IdentifierPart"
and "WhiteSpace" productions are defined based on HTML5, not based on ES
so as to avoid element and attribute names composed purely of combining
characters and zero-width joiners, and to avoid treating BOM as white-space.

Syntax:

   html-literal := fragment | "@" element
   fragment     := "<>" contents "</>"
   contents     := ( element | element-text | substitution )*;
   element      := ( starttag contents* endtag | emptytag )
   starttag     := "<" elementname [ WhiteSpace attribute ]* ">"
   emptytag     := "<" elementname [ WhiteSpace attribute ]* "/>"
   endtag       := "</" elementname ">"  ; must match elementname of corresponding starttag production
   elementname  := ( "svg:" | "math:" )? IdentifierPart
   attribute    := attribute-s | attribute-b
   attribute-s  := IdentifierPart "=" ( subst-string | substitution )
   attribute-b  := IdentifierPart "?=" ( substitution )
   subst-string :=   "'" ( subst-text-1 | substitution )* "'" 
                   | """ ( subst-text-2 | substitution )* """
   substitution := "{" Expression "}"
   element-text := SourceCharacter but not "<", "{", "\", or LineTerminator
                   | EscapeSequence
                   | LineContinuation
   subst-text-1 := SourceCharacter but not "'", "{", "\", or LineTerminator
                   | EscapeSequence
                   | LineContinuation
   subst-text-2 := SourceCharacter but not """, "{", "\", or LineTerminator
                   | EscapeSequence
                   | LineContinuation
 
   IdentifierPart := (-?[A-Za-z0-9])*;
   WhiteSpace     := ([\t ] | LineTerminator)+;

*/
var e4h = (function () {
  "use strict";
  // Memo-ize our parsing so that the cost of the marginal use is
  // the cost of a cloneNode.
  var siteObjToDOMTemplate = new WeakMap(),
    doc = document;

  return function (siteObj) {  // var_args substitutions

    // First we derive information by parsing (thunkily) the raw text chunks
    // to produce a DOM tree and associated substitution functions.
    var domTemplate = siteObjToDOMTemplate.get(siteObj),
      // Second we clone the DOM tree.
      rootNodeClone,
      // Then we call a function for each interpolated value that mutates the
      // clone.
      substitutions,
      i,
      n;

    // Derive the base DOM tree and related information if necessary.
    if (!domTemplate) { (function () {
      var domStack    = [],
        substitutions = [],
        rawText       = siteObj.raw.slice(0),
        pos           = 0,
        errorMessage  = null,
        valueIndex    = 0,
        nValues       = rawText.length - 1,
        prefixToNamespace = {
          'math' : 'http://www.w3.org/1998/Math/MathML',
          'svg'  : 'http://www.w3.org/2000/svg'
        },
        isFragment,
        rootNode = null;

      // Returns a match if re matches at the beginning of the unprocessed
      // input.
      function lookahead(re) {
        if (!rawText.length) { return null; }
        return rawText[0].match(re);
      }

      // Like lookahead, but consumes the matched content.
      function check(re) {
        if (re.global) { throw new Error(re + ' is global'); }
        var match = lookahead(re), n;
        if (match) {
          n = match[0].length;
          pos += n;
          rawText[0] = rawText[0].substring(n);
        }
        return match;
      }

      // Like check, but aborts processing with an error message if re does
      // not match.
      function require(re, msg) {
        var match = check(re);
        if (!match) {
          throw new SyntaxError(
            (msg || 'Expected ' + re) + ' at char ' + pos
          );
        }
        return match;
      }

      // True when we have finished processing the current chunk of raw text
      // and have a pending interpolation to process.
      function atValue() {
        return valueIndex < nValues && rawText[0] === '';
      }

      // When atValue(), registers the given function as one that operates on
      // a cloned domRoot, and a runtime value to perform a substitution.
      function subst(receiver) {
        rawText.shift();
        substitutions.push(receiver);
      }

      // Returns a function that given a DOM-subtree and other arguments,
      // traverses the sub-tree to reach the equivalent of the top of domStack,
      // and calls f with the same arguments but that equivalent node instead
      // of the zero-th argument.
      function applyToCloneOfTop(f) {
        var childCounts = [], i, last;
        for (i = 0, last = domStack.length - 1; i < last; ++i) {
          childCounts[i] = domStack[i].childNodes.length - 1;
        }
        return function applyToClone(clone) {  // var_args
          var i, n, args, current = clone;
          for (i = 0, n = childCounts.length; i < n; ++i) {
            current = current.childNodes[childCounts[i]];
          }
          args = childCounts.slice.call(arguments, 0);
          args[0] = current;
          f.apply(this, args);
        };
      }

      function decodeEscapeSequences(s) {
        return s.replace(
          /\\(?:u([0-9A-Fa-f]{4})|x([0-9A-Fa-f]{2})|([0-3]?[0-7]{2}|[4-7][0-7]?)|([\s\S]))/g,
          function (_, hex4, hex2, oct, other) {
            var hex = hex4 || hex2;
            if (hex) { return String.fromCharCode(parseInt(hex, 16)); }
            if (oct) { return String.fromCharCode(parseInt(oct, 8)); }
            switch (other) {
            case 'b':
              return '\b';
            case 't':
              return '\t';
            case 'f':
              return '\f';
            case 'v':
              return '\v';
            case 'n':
              return '\n';
            case 'r':
              return '\r';
            default:
              return other;
            }
          }
        );
      }


      //// A recursive-descent parser for the E4H grammar.
      function parseElementText() {
        var text = require(/^(?:[^<\\]|\\[\s\S])+/);
        text = decodeEscapeSequences(text[0]);
        domStack[domStack.length - 1].appendChild(doc.createTextNode(text));
      }

      function parseSubstitutionInText() {
        // TODO: this relies on the stored root node not being normalized
        // if it doesn't escape.
        // Is that safe to assume?
        var placeholder = doc.createTextNode("<PLACEHOLDER>");
        domStack[domStack.length - 1].appendChild(placeholder);
        domStack.push(placeholder);
        subst(applyToCloneOfTop(
          function (cloneOfPlaceholder, x) {
            if (x && 'number' === typeof x.nodeType) {
              // TODO: better test for DOM-node-ness.
              if (x.ownerDocument !== doc) {
                x = doc.importNode(x);
              }
            } else {
              x = doc.createTextNode(String(x));
            }
            cloneOfPlaceholder.parentNode.replaceChild(
              x,
              cloneOfPlaceholder
            );
          }
        ));
        domStack.pop();
      }

      function parseContents() {
        while (true) {
          if (atValue()) {
            parseSubstitutionInText();
          } else {
            if (lookahead(/^[^<]/)) {
              parseElementText();
            } else if (lookahead(/^<[a-zA-Z0-9\-]/)) {
              parseElement();
            } else {
              // return control to caller to find an end-tag or other
              // delimiting construct.
              return;
            }
          }
        }
      }

      function substIntoAttrValue(setValue, chunks, isLast) {
        var chunkIndex = chunks.length;
        return function (el, x) {
          chunks[chunkIndex] = x;
          if (isLast) {
            setValue(el, chunks.join(''));
            // TODO: release chunks filled with values for GC.
          }
        };
      }

      function parseAttribute(element, setAttribute, attrName, operator) {
        function setValue(el, value) {
          setAttribute(el, attrName, value);
        }
        if (operator === '?=') {  // A boolean attribute.
          if (!atValue()) {
            throw new SyntaxError(
              'attr ?= at char ' + pos + ' not followed by expression'
            );
          }
          subst(
            applyToCloneOfTop(
              function (el, b) {
                if (b) {
                  setValue(el, attrName);
                }
              }
            )
          );
        } else if (atValue()) {
          subst(applyToCloneOfTop(setValue));
        } else {
          // Look for quoted section.
          // If it spans a value, then take interpolations into
          // account.  Otherwise just add it to the DOM.
          var delimiter = require(/^[\"\']/)[0],
            chunks = [],
            substitutedInto = false,
            valueBefore,
            chunk,
            isLast;
          while (true) {
            valueBefore = atValue();
            if (valueBefore) {
              rawText.shift();
            }
            chunk = require(
              delimiter === '"'
                ? /^((?:[^\"\\]|\\[\s\S])*)(\")?/
                : /^((?:[^\'\\]|\\[\s\S])*)(\')?/
            );
            isLast = !!chunk[2];
            chunk = decodeEscapeSequences(chunk[1]);
            if (valueBefore) {
              substitutedInto = true;
              substitutions.push(
                applyToCloneOfTop(substIntoAttrValue(setValue, chunks, isLast))
              );
              chunks.push(null);
            }
            chunks.push(chunk);
            if (isLast) { break; }
          }
          if (!substitutedInto) {
            // If we have no substitutions in the attribute, just set it so
            // it will be cloned as normal.
            // TODO: is it OK if <img src> here starts a download even if
            // parsing ultimately fails?
            setValue(element, chunks.join(''));
          }
        }
      }

      function parseElement() {
        var elementStart = pos,
          startTag = require(/^<(?:(svg|math):)?([a-zA-Z0-9\-]+)/),
          prefix = startTag[1],
          elementName = startTag[2],
          namespace = prefix ? prefixToNamespace[prefix] : null,
          element = namespace
            ? doc.createElementNS(namespace, elementName)
            : doc.createElement(elementName),
          setAttribute = function (el, localName, value) {
            if (namespace) {
              el.setAttributeNS(namespace, localName, value);
            } else {
              el.setAttribute(localName, value);
            }
          },
          attr;
        domStack[domStack.length - 1].appendChild(element);
        domStack.push(element);
        while (
          (attr = check(/^[ \t\r\n\u2028\u2029]+([A-Za-z0-9\-]+)(\??=)/))
        ) {
          parseAttribute(element, setAttribute, attr[1], attr[2]);
        }
        (function () {
          var endTag = require(/^[ \t\r\n\u2028\u2029]*(\/?)>/),
            closeTagStart,
            closeTag;
          if (!endTag[1]) {  // not empty tag
            parseContents();
            closeTagStart = pos;
            closeTag = require(/^<\/([A-Za-z0-9\-]+)>/);
            if (closeTag[1] !== elementName) {
              throw new SyntaxError(
                '<' + elementName + '> at char ' + elementStart
                  + ' not properly closed by </' + closeTag[1] + '> at char '
                  + closeTagStart
              );
            }
          }
        }());
        domStack.pop();
      }

      try {
        // Are we parsing a fragment or an element?
        isFragment = check(/^<>/);
        if (!isFragment) { require(/^@/); }

        domStack.push(doc.createDocumentFragment());
        if (isFragment) {
          parseContents();
          require(/^<\/>$/, 'Expected end marker');
        } else {
          parseElement();
        }
        // All input must have been consumed.
        (function () {
          var remainder = rawText.join('');
          if (remainder) {
            throw new SyntaxError(
              'Unparsed content "' + remainder + '" at ' + pos
            );
          }
        }());
      } catch (e) {
        if (e instanceof SyntaxError) {
          errorMessage = e.toString();
        } else {
          console.log(e.stack);
          throw e;
        }
      }

      if (!errorMessage) {
        if (domStack.length === 1) {
          rootNode = domStack[0];
        } else {
          errorMessage = 'Unclosed node ' + domStack[domStack.length - 1];
          substitutions = null;
        }
      }
      if (errorMessage) {
        rootNode = substitutions = null;
      }

      domTemplate = {
        // Store any error message so we can replay it consistently.
        errorMessage  : errorMessage,
        // The node to clone if well-formed.
        rootNode      : rootNode,
        // Explain how to find the node to substitute into.
        substitutions : substitutions,
        // True iff we return the root node, instead of its sole child.
        isFragment    : isFragment
      };

      siteObjToDOMTemplate.set(siteObj, domTemplate);
    }()); }

    if (domTemplate.errorMessage) {
      throw new SyntaxError(domTemplate.errorMessage);
    }
    rootNodeClone = domTemplate.rootNode.cloneNode(true);
    substitutions = domTemplate.substitutions;
    for (i = 0, n = substitutions.length; i < n; ++i) {
      substitutions[i].call(null, rootNodeClone, arguments[i + 1]);
    }
    return domTemplate.isFragment ? rootNodeClone : rootNodeClone.firstChild;
  };
}());
